package com.feidee.data.report.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.feidee.data.report.constant.Constant;
import com.feidee.data.report.dao.IFunnelAnalyseDao;
import com.feidee.data.report.dao.IRedisDao;
import com.feidee.data.report.model.EventsParam;
import com.feidee.data.report.model.FunnelParam;
import com.feidee.data.report.model.FunnelResult;
import com.feidee.data.report.service.IFunnelAnalyseServices;
import com.feidee.data.report.util.FunnelAnalyseThread;
import com.feidee.data.report.util.ToolUtils;
import com.feidee.fdcommon.configuration.CustomConfiguration;
import com.feidee.fdcommon.util.MD5Util;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

@Service("funnelAnalyseService")
public class FunnelAnalyseServicesImpl implements IFunnelAnalyseServices {

    public Logger logger = Logger.getLogger(FunnelAnalyseServicesImpl.class);

    private static ExecutorService funnelAnalyseThreadPool = Executors.newFixedThreadPool(Constant
            .FUNNEL_THREAD_POOL_SIZE);
    @Resource
    private IFunnelAnalyseDao funnelAnalyseDao;
    @Resource
    private IRedisDao redisDao;

    @Override
    public List<FunnelResult> funnelAnalyse(String request_id, FunnelParam funnelParam, String searchSql) {
        List<FunnelResult> funnelResults = new ArrayList<>();
        List<FunnelAnalyseThread> funnelAnalyseTasks = new ArrayList<>();
        String funnel_name = funnelParam.getFunnel_name();
        String end_date = funnelParam.getEnd_date();
        String sqlMd5 = ToolUtils.md5(searchSql);
        int eventNum = funnelParam.getEvents().size();
        try {
            // 获取redis缓存，如果获取到则直接返回
            List<FunnelResult> redisFunnelResults = redisDao.queryFunnelResult(sqlMd5);
            if (redisFunnelResults != null) {
                logger.info("漏斗分析查询名称 " + funnel_name + " 对应的md5: " + sqlMd5 + " redis缓存命中，结果为: " +
                        redisFunnelResults.toString());
                return redisFunnelResults;
            }
            logger.info("漏斗分析查询名称 " + funnel_name + " redis缓存无命中,开始查询clickhouse");
            for (int i = 1; i <= Constant.CLICKHOUSE_NUM; i++) {
                funnelAnalyseTasks.add(new FunnelAnalyseThread(request_id, funnelAnalyseDao, searchSql, eventNum, i));
            }
            List<Future<List<FunnelResult>>> futures = funnelAnalyseThreadPool.invokeAll(funnelAnalyseTasks, Constant
                    .CLICKHOUSE_WAIT_TIME, TimeUnit.SECONDS);
            for (Future<List<FunnelResult>> future : futures) {
                try {
                    List<FunnelResult> funnelResult = future.get();
                    // 只要有一个线程获取结果异常则整个查询失败
                    if (funnelResult == null) {
                        return null;
                    }
                    // 将局部结果汇总到一起
                    addToResultList(funnelResult, funnelResults);
                } catch (Exception e) {
                    logger.error("漏斗分析查询名称 " + funnel_name + " 获取结果超时", e);
                    return null;
                }
            }
            // 如果查询日期在当天之前则不过期缓存进redis，否则过期缓存进redis
            if (ToolUtils.isNotNull(end_date)
                    && (end_date.compareTo(ToolUtils.getCurrentDay()) < 0)) {
                redisDao.insertList(sqlMd5, funnelResults);
            } else {
                redisDao.insertListWithLife(sqlMd5, funnelResults, CustomConfiguration.getInt("funnel.redis.time", 60));
            }
        } catch (Exception e) {
            logger.error("漏斗分析查询名称 " + funnel_name + " 查询失败", e);
            return null;
        }
        logger.info("漏斗分析查询名称 " + funnel_name + " 查询结果为: " + funnelResults.toString());
        return funnelResults;
    }

    /**
     * 根据查询参数获取查询SQL
     *
     * @param funnelParam 漏斗分析请求参数
     * @return clickhouse查询语句
     */
    @Override
    public String getSearchSql(FunnelParam funnelParam) {
        String searchSql = null;
        try {
            int eventNum = funnelParam.getEvents().size();
            String SUMIF_STR = getSumIfStr(eventNum);
            String WINDOW_STR = getWindowStr(funnelParam.getWindow(), funnelParam.getEvents(), eventNum);
            String FILTER_STR = getFilterStr(funnelParam.getFilter());
            String TIME_LIMIT_STR = getTimeLimitStr(funnelParam.getStart_date(), funnelParam.getEnd_date());
            searchSql = "SELECT #SUMIF_STR# FROM(" +
                    "        SELECT level, COUNT() AS count FROM(" +
                    "            SELECT udid, #WINDOW_STR# " +
                    "            FROM #CLICKHOUSE_TABLE_NAME# " +
                    "            PREWHERE #FILTER_STR# AND #TIME_LIMIT_STR# " +
                    "            GROUP BY udid " +
                    "        )" +
                    "    GROUP BY level);";
            searchSql = searchSql.replace("#SUMIF_STR#", SUMIF_STR)
                    .replace("#WINDOW_STR#", WINDOW_STR)
                    .replace("#CLICKHOUSE_TABLE_NAME#", CustomConfiguration.getString("funnel_clickhouse_table",
                            "dw.funnel_loan_event_part"))
                    .replace("#FILTER_STR#", FILTER_STR)
                    .replace("#TIME_LIMIT_STR#", TIME_LIMIT_STR);
        } catch (Exception e) {
            logger.error("漏斗分析 " + funnelParam.getFunnel_name() + " 获取查询SQL失败", e);
        }
        return searchSql;
    }

    /**
     * 获取sumIf条件
     *
     * @param eventNum 事件数量
     * @return 格式：sumIf(count, level >= 1) AS event_1
     */
    private String getSumIfStr(int eventNum) {
        StringBuilder sumIfStrBuilder = new StringBuilder();
        for (int i = 1; i < eventNum; i++) {
            sumIfStrBuilder.append("sumIf(count, level >= ").append(i).append(") AS event_").append(i).append(", ");
        }
        sumIfStrBuilder.append("sumIf(count, level = ").append(eventNum).append(") AS event_").append(eventNum);
        String sumIfStr = sumIfStrBuilder.toString();
        return sumIfStr;
    }

    /**
     * 获取windowFunnel条件
     *
     * @param events   事件集
     * @param eventNum 事件数量
     * @return 格式：windowFunnel(xxx)(client_time, source = 'xxx' AND value = 'xxx', ...) AS level
     */
    private String getWindowStr(long window, List<EventsParam> events, int eventNum) {
        StringBuilder windowStrBuilder = new StringBuilder();
        StringBuilder allRuleStrBuilder = new StringBuilder();
        for (int i = 1; i <= eventNum; i++) {
            //循环获取事件参数
            EventsParam eventsParam = events.get(i - 1);
            //循环获取事件查询参数
            Map<String, String> ruleParam = eventsParam.getRule();
            //本次事件查询拼接的字符串
            StringBuilder thisRuleBuilder = new StringBuilder();
            //是否对本次事件查询的字符串是第一次拼接
            boolean isFirstAppend = true;
            Set<String> fieldNameSet = ruleParam.keySet();
            for (String fieldName : fieldNameSet) {
                if (ToolUtils.isNotNull(fieldName)) {
                    String value = ruleParam.get(fieldName);
                    if (ToolUtils.isNotNull(value)) {
                        if (!isFirstAppend) {
                            thisRuleBuilder.append(" AND ");
                        }
                        // 如果需要查询的字段需要转换成MD5字段查询则处理（某些字段由于值过长已在表中转换成MD5值存储，查询速度会加快）
                        if (Constant.FUNNEL_REPLACE_FIELDS.contains(fieldName)) {
                            if (!fieldNameSet.contains(fieldName + "_md5")) {
                                fieldName += "_md5";
                                value = MD5Util.string2MD5(value);
                            }
                        }
                        thisRuleBuilder.append(fieldName).append(" = '").append(value).append("'");
                        isFirstAppend = false;
                    }
                }
            }
            allRuleStrBuilder = allRuleStrBuilder.append(thisRuleBuilder).append(", ");
        }
        String allRuleStr = allRuleStrBuilder.substring(0, allRuleStrBuilder.length() - 2);
        windowStrBuilder.append("windowFunnel(").append(window)
                .append(")(dt, ").append(allRuleStr).append(") AS level");
        String windowStr = windowStrBuilder.toString();
        return windowStr;
    }

    private String getFilterStr(List<JSONObject> filter) {
        StringBuilder filterStrBuilder = new StringBuilder();
        for (JSONObject filterObject : filter) {
            Set<String> filterKeySet = filterObject.keySet();
            String keyStr = null;
            String valueStr = null;
            String symbol = null;
            for (String key : filterKeySet) {
                String value = filterObject.getString(key);
                if (key.equals("include")) {
                    if (value.equals("true")) {
                        symbol = " = ";
                    } else {
                        symbol = " != ";
                    }
                } else {
                    keyStr = key;
                    valueStr = value;
                }
            }
            filterStrBuilder.append(keyStr).append(symbol).append("'").append(valueStr).append("' AND ");
        }
        if (filterStrBuilder.toString().equals("")) {
            return "1=1";
        }
        String filterStr = filterStrBuilder.substring(0, filterStrBuilder.length() - 5);
        return filterStr;
    }

    private String getTimeLimitStr(String start_date, String end_date) {
        StringBuilder timeLimitStrBuilder = new StringBuilder();
        if (ToolUtils.isNotNull(start_date)) {
            timeLimitStrBuilder.append("ymd >= '").append(start_date).append("'");
        }
        if (ToolUtils.isNotNull(end_date)) {
            if (ToolUtils.isNotNull(start_date)) {
                timeLimitStrBuilder.append(" AND ");
            }
            timeLimitStrBuilder.append("ymd <= '").append(end_date).append("'");
        }
        String timeLimitStr = timeLimitStrBuilder.toString();
        if (timeLimitStr.equals("")) {
            return "1=1";
        }
        return timeLimitStr;
    }

    private void addToResultList(List<FunnelResult> funnelResult, List<FunnelResult> funnelResults) {
        if (funnelResults.isEmpty()) {
            funnelResults.addAll(funnelResult);
        } else {
            Collections.sort(funnelResult);
            Collections.sort(funnelResults);
            int size = funnelResults.size();
            for (int i = 0; i < size; i++) {
                Integer newNum = Integer.valueOf(funnelResult.get(i).getNum());
                Integer totalNum = Integer.valueOf(funnelResults.get(i).getNum());
                totalNum += newNum;
                funnelResults.get(i).setNum(totalNum.toString());
            }
        }
    }

//    public static void main(String[] args) {
//        ArrayList<FunnelResult> funnelResults = new ArrayList<FunnelResult>();
//        FunnelResult funnelResult = new FunnelResult();
//        funnelResult.setIndex("23");
//        funnelResult.setNum("5");
//        FunnelResult funnelResult1 = new FunnelResult();
//        funnelResult1.setIndex("4");
//        funnelResult1.setNum("20");
//        FunnelResult funnelResult2 = new FunnelResult();
//        funnelResult2.setIndex("3");
//        funnelResult2.setNum("30");
//        funnelResults.add(funnelResult);
//        funnelResults.add(funnelResult1);
//        funnelResults.add(funnelResult2);
//        System.out.println(funnelResults.toString());
//        Collections.sort(funnelResults);
//        System.out.println(funnelResults.toString());
//
//        ArrayList<FunnelResult> funnelResults111 = new ArrayList<FunnelResult>();
//        FunnelResult funnelResult111 = new FunnelResult();
//        funnelResult111.setIndex("23");
//        funnelResult111.setNum("2");
//        FunnelResult funnelResult1111 = new FunnelResult();
//        funnelResult1111.setIndex("4");
//        funnelResult1111.setNum("10");
//        FunnelResult funnelResult2222 = new FunnelResult();
//        funnelResult2222.setIndex("3");
//        funnelResult2222.setNum("44");
//        funnelResults111.add(funnelResult111);
//        funnelResults111.add(funnelResult1111);
//        funnelResults111.add(funnelResult2222);
//        System.out.println(funnelResults111.toString());
//        Collections.sort(funnelResults111);
//        System.out.println(funnelResults111.toString());
////		73  30  7
//        List<FunnelResult> funnelResultsTest = new ArrayList<FunnelResult>();
//        FunnelAnalyseServicesImpl funnelAnalyseServices = new FunnelAnalyseServicesImpl();
//        funnelAnalyseServices.addToResultList(funnelResults, funnelResultsTest);
//        System.out.println(funnelResultsTest.toString());
//        funnelAnalyseServices.addToResultList(funnelResults111, funnelResultsTest);
//        System.out.println(funnelResultsTest.toString());
//
//        System.out.println(funnelAnalyseServices.getSumIfStr(3));
//        ArrayList<EventsParam> eventsParams = new ArrayList<EventsParam>();
//        EventsParam eventsParam = new EventsParam();
//        eventsParam.setIndex("1");
//        eventsParam.setName("diyi");
//        RuleParam ruleParam1 = new RuleParam();
//        ruleParam1.setPagevalue("1");
//        ruleParam1.setType("11");
//        eventsParam.setRule(ruleParam1);
//        eventsParams.add(eventsParam);
//
//        EventsParam eventsParam1 = new EventsParam();
//        eventsParam1.setIndex("1");
//        eventsParam1.setName("diyi");
//        RuleParam ruleParam11 = new RuleParam();
//        ruleParam11.setPagevalue("22");
//        ruleParam11.setType("2");
//        eventsParam1.setRule(ruleParam11);
//        eventsParams.add(eventsParam1);
//
//        EventsParam eventsParam11 = new EventsParam();
//        eventsParam11.setIndex("1");
//        eventsParam11.setName("diyi");
//        RuleParam ruleParam111 = new RuleParam();
//        ruleParam111.setPagevalue("3");
//        ruleParam111.setType("33");
//        eventsParam11.setRule(ruleParam111);
//        eventsParams.add(eventsParam11);
//
//        EventsParam eventsParam111 = new EventsParam();
//        eventsParam111.setIndex("1");
//        eventsParam111.setName("diyi");
//        RuleParam ruleParam1111 = new RuleParam();
//        ruleParam1111.setPagevalue("4");
//        ruleParam1111.setType("44");
//        eventsParam111.setRule(ruleParam1111);
//        eventsParams.add(eventsParam111);
//
//        EventsParam eventsParam1111 = new EventsParam();
//        eventsParam1111.setIndex("1");
//        eventsParam1111.setName("diyi");
//        RuleParam ruleParam11111 = new RuleParam();
//        ruleParam11111.setPagevalue("5");
//        ruleParam11111.setSource("555");
//        ruleParam11111.setType("55");
//        ruleParam11111.setValue("555555");
//        eventsParam1111.setRule(ruleParam11111);
//        eventsParams.add(eventsParam1111);
////		System.out.println(funnelAnalyseServices.getMatchStr(eventsParams, eventsParams.size(), null));
//
//        String json = "{\"funnel_name\":\"借款流程转化\",\"start_date\":\"2018-05-01\",\"end_date\":\"2018-05-07\",
// \"filter\":[{\"ostype\":\"ios\",\"include\":\"true\"},{\"os_version\":\"10.0.0\",\"include\":\"true\"}],
// \"events\":[{\"index\":\"1\",\"name\":\"启动app\",\"rule\":{\"source\":\"visit\"}},{\"index\":\"2\",\"name\":\"注册\",
// \"rule\":{\"source\":\"page\",\"type\":\"page_create\",\"value\":\"zhuceActivity\"}},{\"index\":\"3\",
// \"name\":\"绑卡\",\"rule\":{\"source\":\"action\",\"type\":\"action_click\",\"value\":\"关联银行卡\"}},{\"index\":\"4\",
// \"name\":\"贷款首页\",\"rule\":{\"source\":\"page\",\"type\":\"page_create\",\"value\":\"daikuanActivity\"}},
// {\"index\":\"5\",\"name\":\"点击产品\",\"rule\":{\"source\":\"action\",\"type\":\"action_click\",\"value\":\"申请贷款\",
// \"pagevalue\":\"daikuanActivity\"}},{\"index\":\"6\",\"name\":\"借款\",\"rule\":{\"source\":\"page\",
// \"type\":\"page_create\",\"value\":\"jiekuanActivity\"}}]}";
//        FunnelParam funnelParam = JSONObject.parseObject(json, FunnelParam.class);
//        System.out.println(funnelAnalyseServices.getSearchSql(funnelParam));
//    }
}
